package com.zt.questionnaire.common.utils.jwt;

import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator;
import com.zt.questionnaire.common.config.redis.CacheOperator;
import com.zt.questionnaire.common.utils.JsonUtils;
import com.zt.questionnaire.common.utils.RandomGUID;
import com.zt.questionnaire.common.utils.TimeUtils;
import net.minidev.json.JSONObject;

import java.text.ParseException;
import java.util.Map;

/**
 * https://www.cnblogs.com/yan7/p/7857833.html
 * https://www.cnblogs.com/xiangkejin/archive/2018/05/08/9011119.html
 * 用HS256 对称算法加密；
 * 用RS256 非对称算法加密；
 * 1、使用jwt创建登录的token，并可以解析token。
 * 2、登录原则：同一个用户每次登录都会产生一个新的token（生成token的参数里加一唯一码，每次改变），后一次登录删除前一次的token
 * 且token有过期时间，如果不存在说明token过期或者token被顶号登录了。
 * redis的键放在这里保存的目的是，多个微服务之间不用知道redis的键，直接就可以验证（前提是多个微服务必须公用一个redis）
 * 3、如果不通过redis的实现过期时间，通过Map<String,Object> payloadMap实现的话，键请给timeout，本类会去验证token过期
 *
 * @author sy
 */
public class TokenUtils {

    /**
     * 1.创建一个32-byte的密匙
     */

    private static final byte[] secret = "zhengtailogindjsikolkjikolkijswe".getBytes();


    private static long time = TimeUtils.DAY_15;

    private static String name_random_id = "randomId";
    private static String name_time_out = "timeOut";


    /**
     * 生成token，并保存到redis里面，redis的可以本类自己维护。
     * JWSHeader参数：1.加密算法法则,2.类型，3.。。。。。。。
     * 一般只需要传入加密算法法则就可以。
     * 这里则采用HS256
     * <p>
     * JWSAlgorithm类里面有所有的加密算法法则，直接调用。
     * 如果不通过redis的实现过期时间，通过Map<String,Object> payloadMap实现的话，键请给timeout，本类会去验证token过期，时间为结束时间，毫秒
     *
     * @param payloadMap
     * @param redis      空表示token不用保存redis，否则保存redis，用于验证token是否存在于服务器，如果不存在说明token过期或者token被顶号登录了。
     * @param timeOut    过期时间，秒 （3天=3*24*60*60）,null表示这里不需要考虑过期问题，调用者自己处理
     * @return
     * @throws JOSEException
     */
    public static String creatTokenHS256(Map<String, Object> payloadMap, CacheOperator redis, Long timeOut) throws JOSEException {
        String token = creatTokenHS256(payloadMap, redis == null ? timeOut : null);
        if (redis != null) {
            if (timeOut == null) {
                timeOut = time;
            }
            redis.setInt(token, 1, timeOut);
        }
        return token;
    }

    /**
     * 生成token，用HS256 对称算法加密。
     * 这里做了处理，同一个用户每次登录都会产生一个新的token（token的参数里加一唯一码，每次改变）。
     * 如果不通过redis的实现过期时间，通过Map<String,Object> payloadMap实现的话，键请给timeout，本类会去验证token过期，时间为结束时间，毫秒
     *
     * @param payloadMap
     * @param timeOut    过期时间，秒 （3天=3*24*60*60）,null表示这里不需要考虑过期问题，调用者自己处理
     * @return
     * @throws JOSEException
     */
    public static String creatTokenHS256(Map<String, Object> payloadMap, Long timeOut) throws JOSEException {

        //3.先建立一个头部Header
        /**
         * JWSHeader参数：1.加密算法法则,2.类型，3.。。。。。。。
         * 一般只需要传入加密算法法则就可以。
         * 这里则采用HS256
         *
         * JWSAlgorithm类里面有所有的加密算法法则，直接调用。
         */
        //token的参数里加一唯一码，每次改变,保证同一个用户每次登录都会产生一个新的token
        String randomId = RandomGUID.generateToken();
        payloadMap.put(name_random_id, randomId);
        if (timeOut != null && timeOut > 0) {
            long currTime = TimeUtils.getCurrTime();
            payloadMap.put(name_time_out, currTime + timeOut * 1000);
        }

        JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256);

        //建立一个载荷Payload
        Payload payload = new Payload(new JSONObject(payloadMap));

        //将头部和载荷结合在一起
        JWSObject jwsObject = new JWSObject(jwsHeader, payload);

        //建立一个密匙

        JWSSigner jwsSigner = new MACSigner(secret);

        //签名
        jwsObject.sign(jwsSigner);

        //生成token
        return jwsObject.serialize();


    }

    public static void main(String[] args) throws ParseException, JOSEException {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJyYW5kb21JZCI6IkY3NkJEMzE2NjExQjNCNTcyQ0FDRDFGREZDRDAyMTQ2IiwicGhvbmUiOiJBdFV0LWJUSGpjZ0VPdG1lYXdMdzFjdVYxdjJBWFBFNzhiX1d1b0FZNExFMSIsInRhZyI6InB1YmxpY1Rlc3QiLCJpZCI6MTUsInR5cGVfZnJpc3QiOjIsInJlbGF0aW9uIjoyM30.aESdaEY7jveks51v74C4QLrsRI0sBSofZ5OseyGL_AM";
        Map<String, Object> validHS256 = validHS256(token);
        System.out.println(JsonUtils.toString(validHS256));
    }

    /**
     * 解析token，用HS256 对称算法加密。
     * 如果不通过redis的实现过期时间，通过Map<String,Object> payloadMap实现的话，键请给timeout，本类会去验证token过期，时间为结束时间，毫秒
     *
     * @param token
     * @param redis 空表示token没有保存到redis，不用验证
     * @return 生成token时设置的用户信息，返回null表示token错误
     * @throws ParseException
     * @throws JOSEException
     */
    public static Map<String, Object> validHS256(String token, CacheOperator redis) throws ParseException, JOSEException {
        if (redis != null) {
            boolean exists = redis.isExists(token);
            if (!exists) {
                //说明token过期或者token被顶号登录了
                return null;
            }
        }
        return validHS256(token);
    }

    /**
     * 解析token，用HS256 对称算法加密。
     * 如果不通过redis的实现过期时间，通过Map<String,Object> payloadMap实现的话，键请给timeout，本类会去验证token过期，时间为结束时间，毫秒
     *
     * @param token
     * @return 生成token时设置的用户信息，返回null表示token错误
     * @throws ParseException
     * @throws JOSEException
     */
    public static Map<String, Object> validHS256(String token) throws ParseException, JOSEException {
//        解析token
        JWSObject jwsObject = JWSObject.parse(token);
        //建立一个解锁密匙
        JWSVerifier jwsVerifier = new MACVerifier(secret);
        return verify(jwsObject, jwsVerifier);
    }


    //验证token信息
    private static Map<String, Object> verify(JWSObject jwsObject, JWSVerifier jwsVerifier) throws JOSEException {
        //获取到载荷
        Payload payload = jwsObject.getPayload();
        //判断token
        if (jwsObject.verify(jwsVerifier)) {
            JSONObject js = payload.toJSONObject();
            js.remove(name_random_id);
            Long timeOut = (Long) js.remove(name_time_out);
            if (timeOut != null) {
                //检查是否过期
                long currTime = TimeUtils.getCurrTime();
                if (timeOut < currTime) {
                    //过期
                    js = null;
                }
            }
            return js;
        } else {
            return null;
        }
    }

    /**
     * 创建加密key
     */
    public static RSAKey getKey() throws JOSEException {
        RSAKeyGenerator rsaKeyGenerator = new RSAKeyGenerator(2048);
        RSAKey rsaJWK = rsaKeyGenerator.generate();
        return rsaJWK;
    }

    /**
     * 获取token，使用RS256加密。
     * 如果不通过redis的实现过期时间，通过Map<String,Object> payloadMap实现的话，键请给timeout，本类会去验证token过期，时间为结束时间，毫秒
     *
     * @param payloadMap
     * @param rsaJWK
     * @param redis      空表示token没有保存到redis，不用验证
     * @param timeOut    过期时间，秒 （3天=3*24*60*60）,null表示这里不需要考虑过期问题，调用者自己处理
     * @return
     * @throws JOSEException
     */
    public static String creatTokenRS256(Map<String, Object> payloadMap, RSAKey rsaJWK, CacheOperator redis, Long timeOut) throws JOSEException {
        String token = creatTokenRS256(payloadMap, rsaJWK, redis == null ? timeOut : null);
        if (redis != null) {
            if (timeOut == null) {
                timeOut = time;
            }
            redis.setInt(token, 1, timeOut);
        }
        return token;
    }

    /**
     * 获取token，使用RS256加密。
     * 如果不通过redis的实现过期时间，通过Map<String,Object> payloadMap实现的话，键请给timeout，本类会去验证token过期，时间为结束时间，毫秒
     *
     * @param payloadMap
     * @param rsaJWK
     * @param timeOut    过期时间，秒 （3天=3*24*60*60）,null表示这里不需要考虑过期问题，调用者自己处理
     * @return
     * @throws JOSEException
     */
    public static String creatTokenRS256(Map<String, Object> payloadMap, RSAKey rsaJWK, Long timeOut) throws JOSEException {
        //token的参数里加一唯一码，每次改变,保证同一个用户每次登录都会产生一个新的token
        String randomId = RandomGUID.generateToken();
        payloadMap.put(name_random_id, randomId);
        if (timeOut != null && timeOut > 0) {
            long currTime = TimeUtils.getCurrTime();
            payloadMap.put(name_time_out, currTime + timeOut * 1000);
        }
        //私密钥匙
        JWSSigner signer = new RSASSASigner(rsaJWK);

        JWSObject jwsObject = new JWSObject(
            new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(),
            new Payload(new JSONObject(payloadMap))
        );
        //进行加密
        jwsObject.sign(signer);

        String token = jwsObject.serialize();
        return token;
    }

    /**
     * 解析token，使用RS256加密。
     * 如果不通过redis的实现过期时间，通过Map<String,Object> payloadMap实现的话，键请给timeout，本类会去验证token过期
     *
     * @param token
     * @param rsaJWK
     * @return
     * @throws ParseException
     * @throws JOSEException
     */
    public static Map<String, Object> validRS256(String token, RSAKey rsaJWK, CacheOperator redis) throws ParseException, JOSEException {
        if (redis != null) {
            boolean exists = redis.isExists(token);
            if (!exists) {
                //说明token过期或者token被顶号登录了
                return null;
            }
        }
        return validRS256(token, rsaJWK);
    }

    /**
     * 解析token，使用RS256加密。
     * 如果不通过redis的实现过期时间，通过Map<String,Object> payloadMap实现的话，键请给timeout，本类会去验证token过期
     *
     * @param token
     * @param rsaJWK
     * @return
     * @throws ParseException
     * @throws JOSEException
     */
    public static Map<String, Object> validRS256(String token, RSAKey rsaJWK) throws ParseException, JOSEException {
        //获取到公钥
        RSAKey rsaKey = rsaJWK.toPublicJWK();
        JWSObject jwsObject = JWSObject.parse(token);
        JWSVerifier jwsVerifier = new RSASSAVerifier(rsaKey);
        //验证数据
        return verify(jwsObject, jwsVerifier);
    }
}
